Mock 服务¶
单元测试时应该关注当前被测试对象,而往往被测试对象中需要依赖外部服务,为了避免测试代码的复杂度,最好采用mock的方式屏蔽外部依赖的逻辑。
- 通过写一个替代class来mock外部依赖.
- 通过继承并覆盖外部依赖的方法来mock.
- 通过Spy直接使用真实的外部依赖对象.
LoginComponent依赖AuthService服务实现用户登录业务,代码如下
login.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import {Component} from '@angular/core'; import {AuthService} from "./auth.service"; @Component({ selector: 'app-login', template: `<a [hidden]="needsLogin()">Login</a>` }) export class LoginComponent { constructor(private auth: AuthService) { } needsLogin() { return !this.auth.isAuthenticated(); } } |
AuthService服务通过DI注入LoginComponent,当用户未登录时login按钮显示
AuthService代码示例:
auth.service.ts
1 2 3 4 5 | export class AuthService { isAuthenticated(): boolean { return !!localStorage.getItem('token'); } } |
使用真实的AuthService服务直接测试¶
测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import {LoginComponent} from './login.component'; import {AuthService} from "./auth.service"; describe('Component: Login', () => { let component: LoginComponent; let service: AuthService; beforeEach(() => { service = new AuthService();//每个测试用例均实例化真实的AuthService component = new LoginComponent(service);//通过构造器注入到被测试对象中 }); afterEach(() => { //测试结束后清理测试数据 localStorage.removeItem('token'); service = null; component = null; }); it('canLogin returns false when the user is not authenticated', () => { expect(component.needsLogin()).toBeTruthy(); }); it('canLogin returns false when the user is not authenticated', () => { localStorage.setItem('token', '12345'); //人为调整数据以达到测试用例所需条件 expect(component.needsLogin()).toBeFalsy(); }); }); |
由此可以看出,为了测试LoginComponent组件,我们需要了解AuthService中的业务逻辑,违背了单元测试的职责单一原则,给单元测试带来更多的外部依赖因素,增加了测试代码的复杂度。 为了解决这个问题,我们采用如下3种mock方法来屏蔽外部依赖,使得测试代码更为简洁。
一、替代类方式¶
我们创建一个MockAuthService类,定义相同的业务方法,根据测试用例所需条件自由调整业务方法的返回结果。
在测试代码中可以直接抹去真实AuthService的引用,用MockAuthService代替,完全消除对真实服务的依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import {LoginComponent} from './login.component'; class MockAuthService { authenticated = false;//public 属性可以自由修改其值 isAuthenticated() { return this.authenticated; } } describe('Component: Login', () => { let component: LoginComponent; let service: MockAuthService; beforeEach(() => { service = new MockAuthService();//注入mock类 component = new LoginComponent(service); }); afterEach(() => { service = null; component = null; }); it('canLogin returns false when the user is not authenticated', () => { service.authenticated = false; expect(component.needsLogin()).toBeTruthy(); }); it('canLogin returns false when the user is not authenticated', () => { service.authenticated = true; //修改mock对象中的 数据满足测试条件 expect(component.needsLogin()).toBeFalsy(); }); }); |
通过mock类,我们消除了测试代码对外部服务的依赖,变得更清晰、健壮,即使真实的服务代码有变更也不会影响到测试代码的正确执行。
二、继承覆盖方式¶
写替代类自身在很多情况下也很复杂,并且耗时,有时甚至是毫无必要的。利用TypeScript面向对象的特性,我们的替代类可以直接继承真实的服务,通过覆盖特定的方法实现mock。
1 2 3 4 5 6 7 | class MockAuthService extends AuthService { authenticated = false; isAuthenticated() { return this.authenticated; } } |
这种形式下,测试代码任然可以访问真实服务的属性及其它方法,仅仅是当前测试代码所需控制的业务方法被覆盖。 测试代码与方法一是一致的。
三、Spy方式直接使用真实服务¶
Spy是Jasmine框架提供的一个特性,让我们得以控制真实的类、方法及对象让其按我们的意图返回所需的数据。 这种方式我们不需要写过多的mock类,仅仅针对我们所需的部分进行精确控制。
使用Spy的测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import {LoginComponent} from './login.component'; import {AuthService} from "./auth.service"; describe('Component: Login', () => { let component: LoginComponent; let service: AuthService; let spy: any; beforeEach(() => { service = new AuthService();//注入真实的业务对象 component = new LoginComponent(service); }); afterEach(() => { service = null; component = null; }); it('canLogin returns false when the user is not authenticated', () => { spy = spyOn(service, 'isAuthenticated').and.returnValue(false);//在真实服务对象上spy并控制方法的返回值 expect(component.needsLogin()).toBeTruthy(); expect(service.isAuthenticated).toHaveBeenCalled(); //验证服务的确是被调用过 }); it('canLogin returns false when the user is not authenticated', () => { spy = spyOn(service, 'isAuthenticated').and.returnValue(true);//在真实服务对象上spy并控制方法的返回值 expect(component.needsLogin()).toBeFalsy(); expect(service.isAuthenticated).toHaveBeenCalled(); //验证服务的确是被调用过 }); }); |
不管使用哪种方式,唯一的目的就是保持测试代码简洁,并与外部依赖解耦。